﻿using System;
using System.Collections.Generic;
using System.Linq;
using SlamCms.Collections;
using SlamCms.Common;
using SlamCms.Configuration;
using SlamCms.Web.Sitemaps;

namespace SlamCms.Data
{
	public class DataContext : IDisposable
	{
		private bool _disposed;

		#region Constructors

		public DataContext()
			: this(SlamCmsConfiguration.Instance)
		{
		}

		public DataContext(SlamCmsConfiguration configuration)
		{
			Configuration = configuration;
			Database = new Database(this);
			Cache = new DataContextCache(configuration);
			EnsureCache();
		}

		public void EnsureCache()
		{
			// Load cache groups
			LoadCore();
			LoadTags();
			LoadUsers();
		}

		#endregion

		#region Public Properties

		public SlamCmsConfiguration Configuration { get; private set; }

		public Database Database { get; private set; }

		public DataContextCache Cache { get; private set; }

		#endregion

		#region SlamCMS Collections

		public CollectionWithKey<Guid, Site> Sites
		{
			get
			{
				return Cache.Get<CollectionWithKey<Guid, Site>>(Constants.Cache.Site);
			}
		}

		public CollectionWithKey<Guid, SiteCollection> SiteCollections
		{
			get
			{
				return Cache.Get<CollectionWithKey<Guid, SiteCollection>>(Constants.Cache.SiteCollection);
			}
		}

		public CollectionWithKey<Guid, List> Lists
		{
			get
			{
				return Cache.Get<CollectionWithKey<Guid, List>>(Constants.Cache.List);
			}
		}

		public CollectionWithKey<string, Zone> Zones
		{
			get
			{
				return Cache.Get<CollectionWithKey<string, Zone>>(Constants.Cache.Zone);
			}
		}

		public CollectionWithKey<string, ContentType> ContentTypes
		{
			get
			{
				return Cache.Get<CollectionWithKey<string, ContentType>>(Constants.Cache.ContentType);
			}
		}

		public CollectionWithKey<string, FeatureLocation> FeatureLocations
		{
			get
			{
				return Cache.Get<CollectionWithKey<string, FeatureLocation>>(Constants.Cache.FeatureLocation);
			}
		}

		public CollectionWithKey<string, SiteOrListDefault> SiteOrListDefaults
		{
			get
			{
				return Cache.Get<CollectionWithKey<string, SiteOrListDefault>>(Constants.Cache.SiteOrListDefault);
			}
		}

		public CollectionWithKey<string, Tag> Tags
		{
			get
			{
				return Cache.Get<CollectionWithKey<string, Tag>>(Constants.Cache.Tag);
			}
		}

		public IDictionary<string, TagCulture> TagCultures
		{
			get
			{
				return Cache.Get<IDictionary<string, TagCulture>>(Constants.Cache.TagCulture);
			}
		}

		public CollectionWithKey<string, TagCategory> TagCategories
		{
			get
			{
				return Cache.Get<CollectionWithKey<string, TagCategory>>(Constants.Cache.TagCategory);
			}
		}

		public CollectionWithKey<int, ContentItemStatus> Status
		{
			get
			{
				return Cache.Get<CollectionWithKey<int, ContentItemStatus>>(Constants.Cache.Status);
			}
		}

		public CollectionWithKey<int, User> Users
		{
			get
			{
				return Cache.Get<CollectionWithKey<int, User>>(Constants.Cache.User);
			}
		}

		public CollectionWithKey<int, Group> Groups
		{
			get
			{
				return Cache.Get<CollectionWithKey<int, Group>>(Constants.Cache.Group);
			}
		}

		public CollectionWithKey<string, TagTree> TagTrees
		{
			get
			{
				return Cache.Get<CollectionWithKey<string, TagTree>>(Constants.Cache.TagTree);
			}
		}

		#endregion

		#region Load Collections

		protected void LoadCore()
		{
			LoadCoreCollections();
			LoadFeatureLocations();
		}

		protected void LoadTags()
		{
			LoadTagsAndTrees();
			LoadDefaultTags();
			LoadContentTypeTags();
		}

		protected void LoadUsers()
		{
			LoadUsersAndGroups();
		}

		protected void LoadCoreCollections()
		{
			if (Cache.Contains(Constants.Cache.SiteOrListDefault, false) &&
				Cache.Contains(Constants.Cache.List, false) &&
				Cache.Contains(Constants.Cache.Status, false) &&
				Cache.Contains(Constants.Cache.Site, false) &&
				Cache.Contains(Constants.Cache.SiteCollection, false) &&
				Cache.Contains(Constants.Cache.Zone, false) &&
				Cache.Contains(Constants.Cache.ContentType, false))
				return;

			var status = RetrieveCollectionWithKey<int, ContentItemStatus>(x => x.StatusID, "Status");
			var lists = RetrieveCollectionWithKey<Guid, List>(x => x.ListId, "[SLAM].[List]");
			var sites = RetrieveCollectionWithKey<Guid, Site>(x => x.SiteId, "[SLAM].[Site]");
			var siteCollections = RetrieveCollectionWithKey<Guid, SiteCollection>(x => x.SiteCollectionId, "[SLAM].[SiteCollection]");
			var zones = RetrieveCollectionWithKey<string, Zone>(x => x.ZoneId);
			var contentTypes = RetrieveCollectionWithKey<string, ContentType>(x => x.ContentTypeId);
			var siteOrListDefaults = RetrieveCollectionWithKey<string, SiteOrListDefault>(x => x.SiteOrListDefaultId);

			LoadSitesAndZones(sites, zones);
			LoadSitesAndSiteCollections(sites, siteCollections);
			LoadListsAndSites(lists, sites);

			Cache.Save(Constants.Cache.Status, status);
			Cache.Save(Constants.Cache.List, lists);
			Cache.Save(Constants.Cache.Site, sites);
			Cache.Save(Constants.Cache.SiteCollection, siteCollections);
			Cache.Save(Constants.Cache.Zone, zones);
			Cache.Save(Constants.Cache.ContentType, contentTypes);
			Cache.Save(Constants.Cache.SiteOrListDefault, siteOrListDefaults);
		}

		protected void LoadSitesAndZones(CollectionWithKey<Guid, Site> sites, CollectionWithKey<string, Zone> zones)
		{
			var zoneSites = Database.RetrieveAll<ZoneSite>();

			// sites and zones (many to many)
			foreach (var siteGroup in zoneSites.GroupBy(x => x.ZoneID))
			{
				var zone = zones.Find(siteGroup.Key);
				if (zone == null)
					throw new Exception("Could not find zone {0}".F(siteGroup.Key));

				var siteIds = siteGroup.Select(x => x.SiteID).ToArray();
				if (siteIds != null && siteIds.Length > 0)
				{
					var sitesForZone = sites.Find(siteIds).ToList();
					zone.Sites = sitesForZone;

					foreach (var site in sitesForZone)
					{
						if (site.Zones == null)
							site.Zones = new List<Zone>();

						site.Zones.Add(zone);
					}
				}
				else
				{
					zone.Sites = new List<Site>();
				}
			}
		}

		protected void LoadSitesAndSiteCollections(CollectionWithKey<Guid, Site> sites, CollectionWithKey<Guid, SiteCollection> siteCollections)
		{
			// sites and sitecollections
			foreach (var siteGroup in sites.GroupBy(x => x.SiteCollectionId))
			{
				var siteCollection = siteCollections.Find(siteGroup.Key);

				if (siteCollection == null)
					throw new Exception("Could not find site collection {0}".F(siteGroup.Key));

				foreach (var site in siteGroup)
				{
					site.SiteCollection = siteCollection;
				}

				siteCollection.Sites = siteGroup.ToList();
			}
		}

		protected void LoadListsAndSites(CollectionWithKey<Guid, List> lists, CollectionWithKey<Guid, Site> sites)
		{
			// lists and sites
			foreach (var listGroup in lists.GroupBy(x => x.SiteId))
			{
				var site = sites.Find(listGroup.Key);

				if (site == null)
					throw new Exception("Could not find site {0}".F(listGroup.Key));

				foreach (var list in listGroup)
				{
					list.Site = site;
				}

				site.Lists = listGroup.ToList();
			}
		}

		protected void LoadUsersAndGroups()
		{
			if (Cache.Contains(Constants.Cache.Group, false) &&
				Cache.Contains(Constants.Cache.User, false))
				return;

			var userGroup = Database.RetrieveAll<UserGroup>();
			var tagOwner =Database.RetrieveAll<TagOwner>();

			var users = RetrieveCollectionWithKey<int, User>(x => x.UserId);
			var groups = RetrieveCollectionWithKey<int, Group>(x => x.GroupId);
			var groupSites = Database.RetrieveAll<GroupSite>();
			var sites = Cache.Get<CollectionWithKey<Guid, Site>>(Constants.Cache.Site);

			// Load sites on groups
			foreach (var sitesByGroup in groupSites.GroupBy(x => x.GroupId))
			{
				var group = groups.Find(sitesByGroup.Key);

				if (group == null)
					throw new Exception("Could not find group {0}".F(sitesByGroup.Key));

				var siteIds = sitesByGroup.Select(x => x.SiteId).ToArray();
				if (siteIds != null && siteIds.Length > 0)
				{
					var sitesForGroup = sites.Find(siteIds).ToList();
					group.Sites = sitesForGroup;
				}
				else
				{
					group.Sites = new List<Site>();
				}
			}

			//Load Groups for each User associated with a Group in UserGroup
			foreach (var groupsByUser in userGroup.GroupBy(x => x.UserId))
			{
				var user = users.Find(groupsByUser.Key);

				if (user == null)
					throw new Exception("Could not find user {0}".F(groupsByUser.Key));

				var groupIds = groupsByUser.Select(x => x.GroupId).ToArray();
				if (groupIds != null && groupIds.Length > 0)
				{
					var userGroups = groups.Find(groupIds).ToList();
					user.Groups = userGroups;
				}
				else
				{
					user.Groups = new List<Group>();
				}
			}

			//Load Tags for each User associated with a Tag in TagOwner
			foreach (var tagsByUser in tagOwner.GroupBy(x => x.UserId))
			{
				var user = users.Find(tagsByUser.Key);

				if (user == null)
					throw new Exception("Could not find user {0}".F(tagsByUser.Key));

				var tagIds = tagsByUser.Select(x => x.TagId).ToArray();
				if (tagIds != null && tagIds.Length > 0)
				{
					var userOwnedTags = Tags.Find(tagIds).ToList();
					user.OwnedTags = userOwnedTags;
				}
				else
				{
					user.OwnedTags = new List<Tag>();
				}
			}

			Cache.Save(Constants.Cache.User, users);
			Cache.Save(Constants.Cache.Group, groups);
		}

		protected void LoadFeatureLocations()
		{
			if (Cache.Contains(Constants.Cache.FeatureLocation, false))
				return;

			var featureLocations = RetrieveCollectionWithKey<string, FeatureLocation>(x => x.FeatureLocationId);
			var featureLocationScope = Database.RetrieveAll<FeatureLocationScope>();

			foreach (var scopesByFeatureLocation in featureLocationScope.GroupBy(x => x.FeatureLocationId))
			{
				var featureLocation = featureLocations.Find(scopesByFeatureLocation.Key);

				if (featureLocation == null)
					throw new Exception("Could not find feature location {0}".F(scopesByFeatureLocation.Key));

				var contentTypeIds = scopesByFeatureLocation.Select(x => x.ContentTypeId).ToArray();
				if (contentTypeIds != null && contentTypeIds.Length > 0)
				{
					var featureLocationScopes = ContentTypes.Find(contentTypeIds).ToList();
					featureLocation.Scopes = featureLocationScopes;
				}
				else
				{
					featureLocation.Scopes = new List<ContentType>();
				}
			}

			Cache.Save(Constants.Cache.FeatureLocation, featureLocations);
		}

		protected void LoadDefaultTags()
		{
			if (Cache.Contains(Constants.Cache.DefaultTag, false) )
				return;

			var defaultTags = Database.RetrieveAll<DefaultTag>();

			foreach (var tagsBySiteOrList in defaultTags.GroupBy(x => x.SiteOrListDefaultId))
			{
				var siteOrListDefault = SiteOrListDefaults.Find(tagsBySiteOrList.Key);

				if (siteOrListDefault == null)
					throw new Exception("Could not find site or list default {0}".F(tagsBySiteOrList.Key));

				var tagIds = tagsBySiteOrList.Select(x => x.TagId).ToArray();
				if (tagIds != null && tagIds.Length> 0)
				{
					var siteOrListDefaultDefaultTags = Tags.Find(tagIds).ToList();
					siteOrListDefault.DefaultTags = siteOrListDefaultDefaultTags;
				}
				else
				{
					siteOrListDefault.DefaultTags = new List<Tag>();
				}
			}

			Cache.Save(Constants.Cache.DefaultTag, defaultTags);
		}

		protected void LoadContentTypeTags()
		{
			if (Cache.Contains(Constants.Cache.ContentTypeTag, false))
				return;

			var contentTypeTags = Database.RetrieveAll<ContentTypeTag>();
			foreach (var contentTypeTag in contentTypeTags.GroupBy(x => x.ContentTypeId))
			{
				var contentType = ContentTypes.Find(contentTypeTag.Key);

				if (contentType == null)
					throw new Exception("Could not find content type {0}".F(contentTypeTag.Key));

				var tagIds = contentTypeTag.Select(x => x.TagId).ToArray();
				if (tagIds != null && tagIds.Length > 0)
				{
					var contentTypeItemTags = Tags.Find(tagIds).ToList();
					contentType.DefaultTags = contentTypeItemTags;
				}
				else
				{
					contentType.DefaultTags = new List<Tag>();
				}
			}

			Cache.Save(Constants.Cache.ContentTypeTag, contentTypeTags);
		}

		protected void LoadTagsAndTrees()
		{
			if (Cache.Contains(Constants.Cache.TagCategory, false) &&
				Cache.Contains(Constants.Cache.Tag, false) &&
				Cache.Contains(Constants.Cache.TagCulture, false) &&
				Cache.Contains(Constants.Cache.TagCategoryCulture, false) &&
				Cache.Contains(Constants.Cache.TagTree, false))
				return;

			var tagCategories = RetrieveCollectionWithKey<string, TagCategory>(x => x.TagCategoryId, null, "DisplayOrder");
			var tags = RetrieveCollectionWithKey<string, Tag>(x => x.TagId);
			var tagScope = Database.RetrieveAll<TagScope>();
			var tagCultures = Database.RetrieveAll<TagCulture>().ToDictionary(x => x.TagId + "_" + x.Culture);
			var tagCategoryCultures = Database.RetrieveAll<TagCategoryCulture>().ToDictionary(x => x.TagCategoryId + "_" + x.Culture);

			foreach (var scopesByTag in tagScope.GroupBy(x => x.TagId))
			{
				var tag = tags.Find(scopesByTag.Key);

				if (tag == null)
					throw new Exception("Could not find tag {0}".F(scopesByTag.Key));

				var contentTypeIds = scopesByTag.Select(x => x.ContentTypeId).ToArray();
				if (contentTypeIds != null && contentTypeIds.Length > 0)
				{
					var tagScopes = ContentTypes.Find(contentTypeIds).ToList();
					tag.Scopes = tagScopes;
				}
				else
				{
					tag.Scopes = new List<ContentType>();
				}
			}

			Cache.Save(Constants.Cache.TagCategory, tagCategories);
			Cache.Save(Constants.Cache.Tag, tags);
			Cache.Save(Constants.Cache.TagCulture, tagCultures);
			Cache.Save(Constants.Cache.TagCategoryCulture, tagCategoryCultures);

			var tagTreeBuilder = new TagTreeBuilder(this);
			var tagTrees = new CollectionWithKey<string, TagTree>(x => x.TagTreeId, tagTreeBuilder.BuildAll());

			Cache.Save(Constants.Cache.TagTree, tagTrees);
		}

		#endregion

		#region Protected Methods

		protected CollectionWithKey<TKey, TValue> RetrieveCollectionWithKey<TKey, TValue>(Func<TValue, TKey> keySelector, string tableName = null, string orderBy = "") where TValue : class, new()
		{
			var list = Database.RetrieveAll<TValue>(tableName ?? typeof(TValue).Name, orderBy);
			return new CollectionWithKey<TKey, TValue>(keySelector, list);
		}

		protected IList<Tag> RetrieveTags()
		{
			IList<Tag> tags = Database.RetrieveAll<Tag>(null, "DisplayName").ToList();
			tags = OrderTagsByPopularity(tags);
			return tags;
		}

		protected IList<Tag> OrderTagsByPopularity(IList<Tag> tags)
		{
			if (!ConfigurationHelpers.GetEnvironmentSetting(Constants.Settings.TagPopularityEnabled, true))
				return tags;

			var tagPopularityDays = ConfigurationHelpers.GetEnvironmentSetting(Constants.Settings.TagPopularityDays, 30);
			var tagPopularities = Database.RetrieveTagPopularities(tagPopularityDays);

			TagPopularity tagPopularity;
			foreach (var tag in tags)
			{
				if (tagPopularities.TryGetValue(tag.TagId, out tagPopularity))
					tag.Order = tagPopularity.Popularity;
			}

			return tags.OrderByDescending(t => t.Order).ToList();
		}

		protected T GetSitemapInternal<T>(string sitemapId = null) where T : Sitemap
		{
			if (sitemapId == null)
			{
				sitemapId = typeof(T).Name;
				// Remove the word sitemap
				if (sitemapId.EndsWith("Sitemap", StringComparison.InvariantCultureIgnoreCase))
					sitemapId = sitemapId.Remove(sitemapId.LastIndexOf("Sitemap", StringComparison.InvariantCultureIgnoreCase));
			}

			var sitemap = Cache.Get<T>("SITEMAP_{0}".F(sitemapId));

			if (sitemap == null)
			{
				sitemap = Sitemap.Build<T>(this, sitemapId);
				Cache.Save("SITEMAP_{0}".F(sitemapId), sitemap);
			}

			return sitemap;
		}


		#endregion

		/// <summary>
		/// Creates a new SlamCms content query builder
		/// </summary>
		/// <returns></returns>
		public QueryBuilder CreateQuery()
		{
			var query = new QueryBuilder(this);

			if (ConfigurationHelpers.GetEnvironmentSetting(Constants.Settings.ContentExpirationEnabled, true)
				&& !ConfigurationHelpers.GetEnvironmentSetting(Constants.Settings.ContentExpirationShowExpiredContent, false))
				query.FilterStatus(FilterStatus.LiveAndNotExpired);
			else
				query.FilterStatus(FilterStatus.Live);

			return query;
		}

		public TagTree GetTagTree(string zoneId = null)
		{
			TagTree tagTree;

			if (zoneId.IsNullOrEmpty())
				tagTree = TagTrees.FirstOrDefault();
			else
			{
				var zone = Zones.Where(z => z.ZoneId == zoneId).FirstOrDefault();

				if (zone == null)
					throw new Exception("ZoneId {0} not found.".F(zoneId));

				tagTree = TagTrees.Where(t => t.TagTreeId == zone.TagTreeId).FirstOrDefault();
			}

			if (tagTree == null)
				// TODO regenerate??
				throw new Exception("Could not find tag tree.");

			var tagTreeClone = (TagTree)tagTree.Clone();
			tagTreeClone.Context = this;
			return tagTreeClone;
		}

		public Sitemap GetSitemap(string sitemapId)
		{
			return GetSitemap<Sitemap>(sitemapId);
		}

		public T GetSitemap<T>(string sitemapId) where T : Sitemap
		{
			return GetSitemapInternal<T>(sitemapId);
		}

		public void Dispose()
		{
			this.Dispose(true);
			GC.SuppressFinalize(this);
		}

		protected virtual void Dispose(bool disposing)
		{
			if (this._disposed)
				return;
			this._disposed = true;
			if (!disposing)
				return;
		}
	}
}